package simpledb;

import java.io.IOException;
import java.util.*;

/**
 * BufferPool manages the reading and writing of pages into memory from
 * disk. Access methods call into it to retrieve pages, and it fetches
 * pages from the appropriate location.
 * <p>
 * The BufferPool is also responsible for locking;  when a transaction fetches
 * a page, BufferPool checks that the transaction has the appropriate
 * locks to read/write the page.
 * 
 * @Threadsafe, all fields are final
 */
public class BufferPool {
    /** Bytes per page, including header. */
    private static final int DEFAULT_PAGE_SIZE = 4096;

    private static int pageSize = DEFAULT_PAGE_SIZE;
    
    /** Default number of pages passed to the constructor. This is used by
    other classes. BufferPool should use the numPages argument to the
    constructor instead. */
    public static final int DEFAULT_PAGES = 50;

    private LRUCache cache;
    private int numPages;
    private LockManager lockManager;
    private static int sleepTime = 500;
    /**
     * Creates a BufferPool that caches up to numPages pages.
     *
     * @param numPages maximum number of pages in this buffer pool.
     */
    public BufferPool(int numPages) {
        this.numPages = numPages;
        this.cache = new LRUCache(DEFAULT_PAGES);
        this.lockManager = new LockManager();
        // some code goes here
    }
    
    public static int getPageSize() {
      return pageSize;
    }
    
    // THIS FUNCTION SHOULD ONLY BE USED FOR TESTING!!
    public static void setPageSize(int pageSize) {
    	BufferPool.pageSize = pageSize;
    }
    
    // THIS FUNCTION SHOULD ONLY BE USED FOR TESTING!!
    public static void resetPageSize() {
    	BufferPool.pageSize = DEFAULT_PAGE_SIZE;
    }

    /**
     * Retrieve the specified page with the associated permissions.
     * Will acquire a lock and may block if that lock is held by another
     * transaction.
     * <p>
     * The retrieved page should be looked up in the buffer pool.  If it
     * is present, it should be returned.  If it is not present, it should
     * be added to the buffer pool and returned.  If there is insufficient
     * space in the buffer pool, a page should be evicted and the new page
     * should be added in its place.
     *
     * @param tid the ID of the transaction requesting the page
     * @param pid the ID of the requested page
     * @param perm the requested permissions on the page
     * 使用pgid2pg来确定缓冲池内是否存在该表
     * 有直接取，没有则用heapFile的readPage（）
     * 使用LRUCache的规则作为页面回收策略
     */
    public  Page getPage(TransactionId tid, PageId pid, Permissions perm)
        throws TransactionAbortedException, DbException {
//        if(pgid2pg.containsKey(pid))
//        {
//            return pgid2pg.get(pid);
//        }
//        else
//        {
//            HeapFile heapFile = (HeapFile) Database.getCatalog().getDatabaseFile(pid.getTableId());
//            Page new_page = heapFile.readPage(pid);
//            addNewPage(new_page,pid);
//            return pgid2pg.get(pid);
//        }
        boolean res = perm==Permissions.READ_ONLY?lockManager.grantSLock(tid,pid):lockManager.grantXLock(tid,pid);
        while(!res)
        {
            if(lockManager.isDeadLock(tid,pid))
              throw new TransactionAbortedException();
            try {
                    Thread.sleep(sleepTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            res = perm==Permissions.READ_ONLY?lockManager.grantSLock(tid,pid):lockManager.grantXLock(tid,pid);
        }
        Page page = cache.get(pid);
        if(page!=null)
        {
            return page;
        }
        else
        {
            DbFile heapFile = (DbFile) Database.getCatalog().getDatabaseFile(pid.getTableId());
            Page new_page = heapFile.readPage(pid);
            addNewPage(new_page,pid);
        }
        return cache.getPageById(pid);
    }

    public void addNewPage(Page page,PageId pageId)
    {
        try {
            cache.put(pageId,page);
        } catch (DbException e) {
            e.printStackTrace();
        }
    }
    /**
     * Releases the lock on a page.
     * Calling this is very risky, and may result in wrong behavior. Think hard
     * about who needs to call this and why, and why they can run the risk of
     * calling it.
     *
     * @param tid the ID of the transaction requesting the unlock
     * @param pid the ID of the page to unlock
     * 解除对应的事务上对应page的锁
     */
    public  void releasePage(TransactionId tid, PageId pid) {
        // some code goes here
        // not necessary for lab1|lab2
        lockManager.releaseLocksByPid(pid);
    }

    /**
     * Release all locks associated with a given transaction.
     *
     * @param tid the ID of the transaction requesting the unlock
     * 提交（commit）指定事务所有页面操作
     */
    public void transactionComplete(TransactionId tid) throws IOException {
        // some code goes here
        // not necessary for lab1|lab2
        transactionComplete(tid,true);
    }

    /** Return true if the specified transaction has a lock on the specified page */
    public boolean holdsLock(TransactionId tid, PageId p) {
        // some code goes here
        // not necessary for lab1|lab2
        Map<PageId, List<LockState>> pageLocks = lockManager.getPageLocks();
        if(pageLocks.containsKey(p))
        {
            List<LockState> lockStates = pageLocks.get(p);
            for (LockState lockState:lockStates)
            {
                if(lockState.getTid().equals(tid))
                {
                    return true;
                }
            }
            return false;
        }
        else
            return false;
    }

    /**
     * Commit or abort a given transaction; release all locks associated to
     * the transaction.
     *
     * @param tid the ID of the transaction requesting the unlock
     * @param commit a flag indicating whether we should commit or abort
     *               提交commit/废除abort(撤销所有操作，将dirty page上进行的操作回退为磁盘里page的状态) tid事务的所有操作
     *               参数commit：true 提交commit
     *                           false 废除abort
     */
    public void transactionComplete(TransactionId tid, boolean commit)
        throws IOException {
        // some code goes here
        // not necessary for lab1|lab2

        //commit所有操作将dirty page全部写入磁盘
        if(commit)
        {
            flushPages(tid);
        }
        //将dirty page上所作的操作全部撤销
        else
        {
            List<PageId> pages = lockManager.getAllPagesByTid(tid);
            for (PageId pageId:pages)
            {
                HeapFile heapFile = (HeapFile) Database.getCatalog().getDatabaseFile(pageId.getTableId());
                Page diskPage = heapFile.readPage(pageId);
                try {
                    cache.put(pageId,diskPage);
                } catch (DbException e) {
                    e.printStackTrace();
                }
                diskPage.markDirty(false,tid);
                releasePage(tid,pageId);
            }
        }
    }

    /**
     * Add a tuple to the specified table on behalf of transaction tid.  Will
     * acquire a write lock on the page the tuple is added to and any other 
     * pages that are updated (Lock acquisition is not needed for lab2). 
     * May block if the lock(s) cannot be acquired.
     * 
     * Marks any pages that were dirtied by the operation as dirty by calling
     * their markDirty bit, and adds versions of any pages that have 
     * been dirtied to the cache (replacing any existing versions of those pages) so 
     * that future requests see up-to-date pages. 
     *
     * @param tid the transaction adding the tuple
     * @param tableId the table to add the tuple to
     * @param t the tuple to add
     *                调用HeapFile.insertTuple
     *                将返回的被DirtyPage在缓冲池中更新
     *                问题：是所有DirtyPage都加入缓冲池  还是只更新本来已经存在于缓冲池中的page
     */
    public void insertTuple(TransactionId tid, int tableId, Tuple t)
        throws DbException, IOException, TransactionAbortedException {
        // some code goes here
        // not necessary for lab1
        DbFile dbFile = Database.getCatalog().getDatabaseFile(tableId);
        ArrayList<Page> dirtyPages = dbFile.insertTuple(tid, t);
        Iterator<Page> iterator = dirtyPages.iterator();
        while(iterator.hasNext())
        {
            HeapPage page = (HeapPage) iterator.next();
            if(cache.hasPageById(page.getId()))
            {
                cache.put(page.getId(),page);
            }
        }
    }

    /**
     * Remove the specified tuple from the buffer pool.
     * Will acquire a write lock on the page the tuple is removed from and any
     * other pages that are updated. May block if the lock(s) cannot be acquired.
     *
     * Marks any pages that were dirtied by the operation as dirty by calling
     * their markDirty bit, and adds versions of any pages that have 
     * been dirtied to the cache (replacing any existing versions of those pages) so 
     * that future requests see up-to-date pages. 
     *
     * @param tid the transaction deleting the tuple.
     * @param t the tuple to delete
     *
     *          调用HeapFile.deleteTuple
     *          将返回的被DirtyPage在缓冲池中更新
     *          问题：是所有DirtyPage都加入缓冲池  还是只更新本来已经存在于缓冲池中的page
     */
    public  void deleteTuple(TransactionId tid, Tuple t)
        throws DbException, IOException, TransactionAbortedException {
        // some code goes here
        // not necessary for lab1
        int tableId = t.getRecordId().getPageId().getTableId();
        DbFile dbFile = Database.getCatalog().getDatabaseFile(tableId);
        ArrayList<Page> dirtypages = dbFile.deleteTuple(tid, t);
        Iterator<Page> iterator = dirtypages.iterator();
        while (iterator.hasNext()) {
            Page page = iterator.next();
            if (cache.hasPageById(page.getId())) {
                cache.put(page.getId(), page);
            }
        }
    }

        /**
         * Flush all dirty pages to disk.
         * NB: Be careful using this routine -- it writes dirty data to disk so will
         *     break simpledb if running in NO STEAL mode.
         *     将缓冲池中所有dirty page全部写入磁盘
         */
        public synchronized void flushAllPages () throws IOException {
            // some code goes here
            // not necessary for lab1
            for (Map.Entry entry : cache.getCache().entrySet()) {
                LRUCache.LinkedNode linkedNode = (LRUCache.LinkedNode) entry.getValue();
                if (linkedNode.page.isDirty() != null) {
                    flushPage(linkedNode.page.getId());
                }
            }
        }

        /** Remove the specific page id from the buffer pool.
         Needed by the recovery manager to ensure that the
         buffer pool doesn't keep a rolled back page in its
         cache.
         恢复管理器需要它来确保缓冲池不会在其缓存中保留回滚页面。
         Also used by B+ tree files to ensure that deleted pages
         are removed from the cache so they can be reused safely
         */
        public synchronized void discardPage (PageId pid){
            // some code goes here
            // not necessary for lab1

        }

        /**
         * Flushes a certain page to disk
         * @param pid an ID indicating the page to flush
         *            如果当前页面是dirty,则将其写入磁盘
         */
        private synchronized void flushPage (PageId pid) throws IOException {
            // some code goes here
            // not necessary for lab1
            Page page = cache.getPageById(pid);
            if (page != null) {
                if (page.isDirty() != null) {
                    DbFile dbFile = Database.getCatalog().getDatabaseFile(page.getId().getTableId());
                    if (cache.hasPageById(pid)) {
                        dbFile.writePage(page);
                        page.markDirty(false, null);
                        try {
                            cache.put(page.getId(),page);
                        } catch (DbException e) {
                            e.printStackTrace();
                        }
                    }
                }
            } else
                throw new NoSuchElementException();
        }

        /** Write all pages of the specified transaction to disk.
         * 将给定事务进行操作的所有dirty page写入磁盘
         */
        public synchronized void flushPages (TransactionId tid) throws IOException {
            // some code goes here
            // not necessary for lab1|lab2
            List<PageId> pages = lockManager.getAllPagesByTid(tid);
            for (PageId pageId : pages) {
                flushPage(pageId);
                releasePage(tid, pageId);
            }
        }

        /**
         * Discards a page from the buffer pool.
         * Flushes the page to disk to ensure dirty pages are updated on disk.
         * 在LRUCache中实现了回收策略，这里无需再实现
         */
        private synchronized void evictPage () throws DbException {
            // some code goes here
            // not necessary for lab1
        }

}
